package osmedile.intellij.checkmyvars;

import checkmyvars.Default;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiArrayType;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.util.ClassUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * TODO add support for type: "collection_strict"
 *
 * @author Olivier Smedile
 * @version $Id: ClassTag.java 43 2008-07-11 14:02:05Z osmedile $
 */
public class VarChecker  {

    private String jdocCondition;

    /**
     * name of the annotation
     */
    private String annotation;

    /**
     * The type of the parameter
     */
    private String type;

    /**
     * Template to use, if this varChecker is valid
     */
    private String template;


    /**
     * True if class tag is currently active. If false it won't be processed
     */
    private boolean active;

    private List<String> types;
    private Collection<Class> typeClasses;

    /**
     * Group of the VarChecker, this is an arbitrary name used to sort VarChecker
     */
    private String group;
    @NonNls
    public static final String HIBERNATE_GRP = "hibernate";
    @NonNls
    public static final String COMMON_GRP = "common";
    public static final String JDK14_GRP = "jdk14";
    public static final String NUMBER_GRP = "number";
    public static final String STRING_GRP = "string conversion";

// --------------------------- CONSTRUCTORS ---------------------------


    public VarChecker() {
        this(null, null, null);
    }
    public VarChecker(VarChecker vc) {
        this(vc.getAnnotation(), vc.getType(), vc.getTemplate(), vc.getGroup(), vc.getActive());
    }

    public VarChecker(@Nullable String annotation, @Nullable String type, @Nullable String template) {
        this(annotation, type, template, COMMON_GRP);
    }

    public VarChecker(@Nullable String annotation,
                      @Nullable String type,
                      @Nullable String template, @Default(COMMON_GRP)String group) {
        this(annotation, type, template, group, true);
    }

    public VarChecker(@Nullable String annotation, @Nullable String type, @Nullable String template,
                      @Default(COMMON_GRP)String group, boolean active) {

        types = new ArrayList<String>();
        typeClasses = new ArrayList<Class>();

        this.annotation = annotation;
        this.template = template;
        this.active = active;
        setGroup(group);

        this.type = type;
        parseType(type);
    }

// --------------------- GETTER / SETTER METHODS ---------------------

    public String getGroup() {
        return group;
    }

    public void setGroup(@Default(COMMON_GRP)String group) {
        if (group == null) {
            group = COMMON_GRP;
        }
        this.group = group;
    }

    public String getJdocCondition() {
        return jdocCondition;
    }

    public void setJdocCondition(String jdocCondition) {
        this.jdocCondition = jdocCondition;
    }

    public boolean isActive() {
        return active;
    }

    public boolean getActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }


    public String getAnnotation() {
        return annotation;
    }


    public String getTemplate() {
        return template;
    }

    public void setTemplate(String template) {
        this.template = template;
    }

    public String getType() {
        return type;
    }

    public void setType(@Default String type) {
        this.type = type;

        parseType(type);
    }

    protected final void parseType(@Default String type) {
        if (type == null) {
            type = "";
        }
        types.clear();
        typeClasses.clear();

        Collections.addAll(types, type.split(";"));
        for (int i = 0; i < types.size(); i++) {
            String t = types.get(i).trim();
            try {
                typeClasses.add(Class.forName(t));
            } catch (ClassNotFoundException e) {
                //not a valid class, 
                // first try with java.lang.
                try {
                    typeClasses.add(Class.forName("java.lang." + t));
                    t = "java.lang." + t;
                } catch (ClassNotFoundException e1) {
                    // may be a keyword

                    //Save the string as lowercase to ease further search
                    t = t.toLowerCase();

                    Class c = getTypeClass(t);
                    if (c != null) {
                        typeClasses.add(c);
                    }
                }
            }
            //resave the modified string (difference from orginal: trim and lowercase if not a class name)
            types.set(i, t);
        }
    }

    public Class getTypeClass(String type) {
        if (type == null) {
            return null;
        } else if ("number".equalsIgnoreCase(type)) {
            return Number.class;
        } else if ("boolean".equalsIgnoreCase(type)) {
            return Boolean.class;
        } else if ("byte".equalsIgnoreCase(type)) {
            return Byte.class;
        } else if ("short".equalsIgnoreCase(type)) {
            return Short.class;
        } else if ("int".equalsIgnoreCase(type) || "Integer".equalsIgnoreCase(type)) {
            return Integer.class;
        } else if ("long".equalsIgnoreCase(type)) {
            return Long.class;
        } else if ("float".equalsIgnoreCase(type)) {
            return Float.class;
        } else if ("double".equalsIgnoreCase(type)) {
            return Double.class;
        } else if ("char".equalsIgnoreCase(type) || "character".equalsIgnoreCase(type)) {
            return Character.class;
        } else if ("date".equalsIgnoreCase(type)) {
            return Date.class;
        } else if ("calendar".equalsIgnoreCase(type)) {
            return Calendar.class;
        } else if ("string".equalsIgnoreCase(type)) {
            return String.class;
        } else if ("collection".equalsIgnoreCase(type)) {
            return Collection.class;
        } else if ("list".equalsIgnoreCase(type)) {
            return List.class;
        } else if ("map".equalsIgnoreCase(type)) {
            return Map.class;
        } else if ("set".equalsIgnoreCase(type)) {
            return Set.class;
        }
        return null;
    }

// --------------------- METHODS ---------------------

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof VarChecker)) {
            return false;
        }

        VarChecker checker = (VarChecker) o;

        if (active != checker.active) {
            return false;
        }
        if (annotation != null ? !annotation.equals(checker.annotation) : checker.annotation != null) {
            return false;
        }
        if (group != null ? !group.equals(checker.group) : checker.group != null) {
            return false;
        }
        if (jdocCondition != null ? !jdocCondition.equals(checker.jdocCondition) :
                checker.jdocCondition != null) {
            return false;
        }
        if (template != null ? !template.equals(checker.template) : checker.template != null) {
            return false;
        }
        if (type != null ? !type.equals(checker.type) : checker.type != null) {
            return false;
        }
        if (typeClasses != null ? !typeClasses.equals(checker.typeClasses) : checker.typeClasses != null) {
            return false;
        }
        if (types != null ? !types.equals(checker.types) : checker.types != null) {
            return false;
        }

        return true;
    }

    public int hashCode() {
        int result;
        result = (jdocCondition != null ? jdocCondition.hashCode() : 0);
        result = 31 * result + (annotation != null ? annotation.hashCode() : 0);
        result = 31 * result + (type != null ? type.hashCode() : 0);
        result = 31 * result + (template != null ? template.hashCode() : 0);
        result = 31 * result + (active ? 1 : 0);
        result = 31 * result + (types != null ? types.hashCode() : 0);
        result = 31 * result + (typeClasses != null ? typeClasses.hashCode() : 0);
        result = 31 * result + (group != null ? group.hashCode() : 0);
        return result;
    }
// -------------------------- OTHER METHODS --------------------------

    public void setAnnotation(String annotation) {
        if (annotation == null) {
            throw new IllegalArgumentException("annotation can't be null");
        }
        this.annotation = annotation;
    }


    public boolean typeMatch(PsiParameter param) {
        if (param.getType().equalsToText(getType())) {
            return true;
        }

        if ((types.contains("number") || types.contains("java.lang.Number"))
                && (
                param.getType().equalsToText("byte")
                        || param.getType().equalsToText("short")
                        || param.getType().equalsToText("int")
                        || param.getType().equalsToText("long")
                        || param.getType().equalsToText("float")
                        || param.getType().equalsToText("double"))
                ) {
            return true;
        } else if (param.isVarArgs() && types.contains("varags")) {
            return true;
        } else if (param.getType() instanceof PsiArrayType && types.contains("array")) {
            return true;
        }
        final Class<?> cls;
        try {
            // .split("<") to handle generics type
            cls = Class.forName(param.getType().getCanonicalText().split("<")[0]);
            for (Class aClass : typeClasses) {
                if (aClass.isAssignableFrom(cls)) {
                    return true;
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            //ignore it
        }

        return false;
    }

    public boolean accept(PsiParameter param) {
        if (getActive() == false) {
            return false;
        }
        if (!StringUtil.isEmptyOrSpaces(getType()) && !typeMatch(param)) {
            return false;
        }

        PsiAnnotation[] annos = param.getAnnotations();

        String annotation = getAnnotation();
        if (annotation.startsWith("@") && annotation.length() > 1) {
            annotation = annotation.substring(1);
        }
        for (PsiAnnotation anno : annos) {
            if (annotation.equals(anno.getQualifiedName())) {
                return true;
            }

            if (annotation.equals(ClassUtil.extractClassName(anno.getQualifiedName()))) {
                return true;
            }
        }
        return getType() != null && getAnnotation() == null;
    }


    public String toString() {
        return "VarChecker{" +
                ", group='" + group + '\'' +
                "annotation='" + annotation + '\'' +
                ", type='" + type + '\'' +
                ", template='" + template + '\'' +
                ", active=" + active +
                '}';
    }
}
